﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using ManagedCuda;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.MegaRay.Core;
using RayDen.RayEngine;
using RayDen.RayEngine.Core.Devices;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Core.Types;
using RayDen.RayEngine.IntersectionEngines;
using RayDen.RayEngine.IntersectionEngines.Cuda;

namespace RayDen.MegaRay.GPU
{
    public class MegarayIntersectionDevice : IDisposable
    {
        protected bool started;
        protected double statsTotalRayCount;
        protected long statsStartTime;
        protected string deviceName;

        public bool IsRunning
        {
            get { return started; }
        }
        protected RayEngineScene scene;

        protected NVContext cudaContext;
        protected Stopwatch wallclock;
        protected double statsDeviceIdleTime;
        protected double statsDeviceTotalTime;
        protected Task intersectionTask;
        protected CancellationTokenSource cancelToken;

        protected InputRayBufferCollection<RayBuffer> todoRayBuffers;
        protected OutputRayBufferCollection<RayBuffer> doneRayBuffers;


        protected CudaDeviceVariable<RayRecord> rays;
        protected CudaDeviceVariable<RayHit> hits;
        protected CudaDeviceVariable<Point> verts;
        protected CudaDeviceVariable<GpuSmitsBVHNode> bvh;
        protected CudaDeviceVariable<TriangleInfo> tris;

        private int nodesCount;

        protected CudaKernel intersectKernel;


        public MegarayIntersectionDevice(RayEngineScene scene, NVContext ctx)
        {
            this.scene = scene;
            wallclock = new Stopwatch();
            this.todoRayBuffers = new InputRayBufferCollection<RayBuffer>();
            this.doneRayBuffers = new OutputRayBufferCollection<RayBuffer>();
            this.started = false;
            if (ctx != null)
            {
                this.cudaContext = ctx;
            }
            else
            {
                this.cudaContext = new NVContext() { Context = new CudaContext(CudaContext.GetMaxGflopsDeviceId()) };
            }
            using (var sr = new StreamReader(@"H:\Git\RayDen\CudaMegaRay\x64\Release\MegarayKernels.cu.ptx"))
            {
                intersectKernel = cudaContext.Context.LoadKernelPTX(sr.BaseStream, "Intersect");
            }

            this.rays = new CudaDeviceVariable<RayRecord>(RayBuffer.RayBufferSize);
            this.hits = new CudaDeviceVariable<RayHit>(RayBuffer.RayBufferSize);
            verts = scene.Vertices.ToArray();
            tris=scene.Triangles.Select(i => i.GetInfo()).ToArray();

            if (GlobalConfiguration.Instance.UseSceneCaching && scene.Cache != null)
            {
                bvh = scene.Cache.BvhData;
                nodesCount = scene.Cache.BvhData.Length;
            }
            else
            {
                var da = new BvhDataAdapter(scene);
                var treeData = da.BuildData();
                bvh = treeData;
                nodesCount = treeData.Length;

            }

            Tracer.TraceLine("BVH Data Size {0:F3} MBytes", (nodesCount * 32f) / (1024f * 1024f));
        }

        public void Dispose()
        {
            var items = new IDisposable[] { rays, hits, tris, verts, cudaContext };

            foreach (var item in items)
            {
                if (item != null)
                {
                    item.Dispose();
                }
            }
        }

        public void SetScene(IRayEngineScene scn)
        {
            Tracer.TraceLine("Setting scene");
            this.scene = (RayEngineScene)scn;
            verts = scene.Vertices.ToArray();
            tris = scene.Triangles.Select(i => i.GetInfo()).ToArray();

            if (GlobalConfiguration.Instance.UseSceneCaching && scene.Cache != null)
            {
                bvh = scene.Cache.BvhData;
                nodesCount = scene.Cache.BvhData.Length;
            }
            else
            {
                var da = new BvhDataAdapter(scene);
                var treeData = da.BuildData();
                bvh = treeData;
                nodesCount = treeData.Length;

            }
        }

        public  void Stop()
        {
            wallclock.Stop();
            started = false;

            if (intersectionTask.Status == TaskStatus.Running)
            {
                cancelToken.Cancel();
                while ((intersectionTask.Status == TaskStatus.Running))
                {
                    
                }
                intersectionTask.Dispose();
                intersectionTask = null;
            }

            this.todoRayBuffers = new InputRayBufferCollection<RayBuffer>();
            this.doneRayBuffers = new OutputRayBufferCollection<RayBuffer>();
        }

        public  void Start()
        {
            if (started)
                return;
            started = true;
            wallclock.Start();
            statsDeviceIdleTime = 0.0;
            statsDeviceTotalTime = 0.0;
            cancelToken = new CancellationTokenSource();
            this.intersectionTask = Task.Factory.StartNew(() => RayIntersection(cancelToken.Token, this), cancelToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Current);
        }
        int threadsPerBlock = 32;

        public  void Trace(RayBuffer rayBuffer)
        {
            cudaContext.Context.SetCurrent();
            var raysArg = rayBuffer.RaysInfo;
            this.rays.CopyToDevice(raysArg);
            GCHandle raysHandle = GCHandle.Alloc(raysArg, GCHandleType.Pinned);

            intersectKernel.BlockDimensions = threadsPerBlock;
            intersectKernel.GridDimensions = (raysArg.Length + threadsPerBlock - 1) / threadsPerBlock;

            intersectKernel.Run(this.rays.DevicePointer, this.hits.DevicePointer, this.verts.DevicePointer, this.tris.DevicePointer, this.bvh.DevicePointer,
                this.scene.Triangles.Count, nodesCount, raysArg.Length);

            this.hits.CopyToHost(rayBuffer.rayHits);
            rayBuffer.traced = true;

            raysHandle.Free();
        }

        protected static void RayIntersection(CancellationToken ct, MegarayIntersectionDevice device)
        {
#if VERBOSE
            try
            {
                
#endif
            Tracer.TraceLine("Intersection device {0}", device.GetType().Name);
            while (!ct.IsCancellationRequested)
            {
                var t1 = 0;// device.wallclock.ElapsedTicks;
                Tuple<int, RayBuffer> rayBuffer = null;

#if OLD 
                if (!device.todoRayBuffers.TryDequeue(out rayBuffer))
                    continue;

#else
                rayBuffer = device.todoRayBuffers.Get();
                /*
                if (device.todoRayBuffers.IsEmpty)
                    continue;
                if (!device.todoRayBuffers.TryDequeue(out rayBuffer))
                {
                    continue;
                }
                 * */
#endif

                var t2 = DateTime.UtcNow.Ticks;
                device.Trace(rayBuffer.Item2);
                //Tracer.TraceLine("Result returns {0} items ", buffer.rayHits.Length);
                //            Tracer.TraceLine("Intersections {0} items ", buffer.rayHits.Count(p => p.index != uint.MaxValue));
                var t3 = device.wallclock.ElapsedTicks;

                device.statsDeviceIdleTime += t2 - t1;
                device.statsDeviceTotalTime += t3 - t1;
                device.statsTotalRayCount += rayBuffer.Item2.RaysInfo.Length;
                device.doneRayBuffers.Put(rayBuffer.Item1,rayBuffer.Item2);
                //device.doneRayBuffers[rayBuffer.Item1].Enqueue(rayBuffer.Item2);
            }

#if VERBOSE
            }
            catch (Exception ex)
            {
                Tracer.TraceLine("Intersection device exception");
                //Tracer.TraceLine("Error triangle {0}", currentTriangleIndex);
                Tracer.TraceLine(ex.Message);
                Tracer.TraceLine(ex.StackTrace);

            }
#endif
        }


        public void PushRayBuffer(RayBuffer rayBuffer, int clientIdx)
        {
            if (clientIdx + 1 > doneRayBuffers.Count)
            {
                while (doneRayBuffers.Count < (clientIdx + 1))
                {
                    doneRayBuffers.AddBuffer();
                }
            }
            todoRayBuffers.Put(new Tuple<int, RayBuffer>(clientIdx, rayBuffer));
        }

        public RayBuffer PopRayBuffer(int info)
        {
//            RayBuffer result;
            return doneRayBuffers.Get(info);
            //return result;
        }
    }
}
